iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
JavaScript

JavaScript為什麼筆記本系列 第 21 篇

Day 21|連續寫三次 setCount(count + 1),為什麼不一定會加 3?

  • 分享至 

  • xImage
  •  

Day 20~~~

count++

雖然真的會改 JavaScript 裡的變數,但它沒有告訴 React:

欸,這個東西改了,畫面也要跟著更新。

所以 React 才需要:

setCount(count + 1)

到這裡都還算合理。

但如果我要一次加 3 呢?

直覺上大概會寫:

setCount(count + 1)
setCount(count + 1)
setCount(count + 1)

三行。

每一行都 + 1。

所以答案應該是 + 3 吧?

結果事情沒有這麼單純。


先做一個看起來很合理的按鈕

先來一個最普通的計數器:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  function addThree() {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
  }

  return (
    <button onClick={addThree}>
      count:{count}
    </button>
  )
}

export default Counter

假設現在:

count = 0

按一次按鈕。

我原本以為 React 會做:

0 + 1 = 1
1 + 1 = 2
2 + 1 = 3

所以畫面最後應該變成:

count:3

但實際上,這三行不能直接這樣理解。

問題就出在:

count

原來這三個 count,看到的是同一個值

Day 20 有講到:

function Counter() {
  const [count, setCount] = useState(0)

Component 每次 render,都會重新執行一次。

而這一次 render 裡面的:

count

可以先想成 React 給我的一張「這次畫面的狀態快照」。

假設目前這一次 render:

count = 0

那這次執行 addThree() 時,裡面的三個 count:

setCount(count + 1)
setCount(count + 1)
setCount(count + 1)

看到的都是:

count = 0

所以比較接近:

setCount(0 + 1)
setCount(0 + 1)
setCount(0 + 1)

也就是:

setCount(1)
setCount(1)
setCount(1)

突然就不奇怪了。

我原本腦中的版本是:

setCount()
↓
count 馬上變成 1
↓
下一行拿到 1
↓
再變 2
↓
下一行拿到 2

但實際上不是這樣。


setCount() 不是把眼前這個 count 立刻改掉

這點其實滿反直覺的。

看到:

setCount(count + 1)

很容易把它腦補成:

count = count + 1

但這兩件事情不一樣。

假設這次 render:

count = 0

執行:

setCount(count + 1)

比較適合我現在的理解是:

目前 count 是 0
↓
算出 0 + 1
↓
告訴 React:
下一次 state 要更新成 1

它不是:

目前 count 是 0
↓
直接把這個 count 改成 1
↓
後面的程式碼開始看到 1

所以即使下一行立刻寫:

console.log(count)

像這樣:

function addOne() {
  setCount(count + 1)

  console.log(count)
}

假設按按鈕以前:

count = 0

這次 function 裡的 console.log(count) 看到的仍然是這次 render 的:

0

因為這次 render 裡的 count 沒有被改掉。

React 是準備產生「下一次 render」。


我開始理解「state 是 snapshot」這句話了

以前看到 state 是 snapshot,我大概就是:

喔,好。

然後下一頁。

但放到這個例子突然比較有感。

可以把每一次 render 想成拍了一張照片。

第一次:

Render #1

count = 0

在這張照片裡:

count

就是 0。

所以這次建立出來的 addThree():

function addThree() {
  setCount(count + 1)
  setCount(count + 1)
  setCount(count + 1)
}

裡面的 count 都是這張照片裡的 0。

因此變成:

setCount(1)
setCount(1)
setCount(1)

等 React 處理完更新,才會產生下一張照片:

Render #2

count = 1

新的 render 裡,才會有新的 count。

所以不是:

一個 count 不斷被修改

而比較像:

Render #1
count = 0
↓
提出 state 更新
↓
Render #2
count = 1

這跟我原本把 React state 當成普通變數的理解,差滿多的。


那我要真的加 3 怎麼辦?

React 還有另一種寫法。

不是直接給它下一個值:

setCount(count + 1)

而是給它一個 function:

setCount(prev => prev + 1)

所以要加三次,可以寫:

setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)

這次就不一樣了。

這裡的:

prev

不是直接去讀這次 render 裡固定的 count。

而是在 React 處理更新時,拿到前一次更新計算完成的結果。

假設原本:

count = 0

第一個:

prev => prev + 1

拿到:

prev = 0

算出:

1

第二個接著拿到:

prev = 1

算出:

2

第三個:

prev = 2

算出:

3

所以整個流程比較像:

0
↓
prev => prev + 1
↓
1
↓
prev => prev + 1
↓
2
↓
prev => prev + 1
↓
3

最後下一次 render:

count = 3

原來這兩種 setCount 不只是寫法不同

以前看到這兩種寫法:

setCount(count + 1)

和:

setCount(prev => prev + 1)

我會覺得:

第二種是不是只是比較潮的寫法?

結果不是。

這兩個其實在表達不同的事情。

第一種:

setCount(count + 1)

比較像:

根據這次 render 裡的 count,算出我要的下一個值。

第二種:

setCount(prev => prev + 1)

比較像:

根據前一個 state 的結果,算出下一個 state。

所以如果新的 state 明確依賴前一個 state:

加一
減一
切換 boolean
在原本陣列上新增資料

就很常會看到 updater function 這種寫法。

例如:

setCount(prev => prev + 1)

或者:

setIsOpen(prev => !prev)

那 React 為什麼不每次 setCount 就立刻 render?

這又是我原本的一個誤解。

三次:

setCount(...)
setCount(...)
setCount(...)

我會想像 React:

setCount
↓
render

setCount
↓
render

setCount
↓
render

但 React 不一定會這樣一行一行馬上重畫。

它可以先把同一段事件裡的 state 更新收集起來,再一起處理。

像:

使用者按下按鈕
↓
事件處理 function 開始
↓
setCount(...)
setCount(...)
setCount(...)
↓
React 收集這些更新
↓
事件處理完成
↓
計算新的 state
↓
重新 render

這件事情通常會看到一個名詞:

batching

也就是「批次處理」。

我目前不打算把 React 的 batching 機制整個挖到底。

先記得一件事就好:

setState 不是叫 React 現在立刻停下一切,把畫面重畫一次。

它比較像是在提出一個 state 更新。

React 再決定怎麼處理這些更新並產生下一次 render。


把 Day 20 和 Day 21 接起來

Day 20 我留下的是:

普通 JavaScript 變數改變
≠
React state 更新

今天又多了一層:

setCount(...)
≠
立刻修改這次 render 裡的 count

所以現在整個流程比較像:

Render #1
count = 0
↓
使用者按按鈕
↓
event handler 執行
↓
提出 state 更新
↓
React 處理更新
↓
Render #2
取得新的 count
↓
JSX 重新計算
↓
畫面更新

而這也解釋了為什麼:

setCount(count + 1)
setCount(count + 1)
setCount(count + 1)

不能單純看成:

+1
+1
+1

因為三個 count 都還活在同一次 render 的世界裡。


今天我真正想記住的

今天最重要的不是背:

setCount(prev => prev + 1)

而是終於比較理解:

一次 render 裡拿到的 state,可以把它想成那一次 render 的 snapshot。

所以:

setCount(count + 1)

不是直接修改眼前的 count。

如果新的 state 需要依賴前一個 state,可以用:

setCount(prev => prev + 1)

讓 React 在處理更新時,一次接著一次算。

於是:

setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)

才會真正形成:

0 → 1 → 2 → 3

寫到這裡,又出現一個很熟的東西。

prev => prev + 1

這到底是什麼?

為什麼 setCount() 裡面突然可以塞一個 function?

而且 React 還會自己把 prev 傳進去?

下一篇再挖。


上一篇
Day 20|我明明 count++了,為什麼畫面沒變?
下一篇
Day 22|setCount(prev => prev + 1) 裡面為什麼可以塞 function?
系列文
JavaScript為什麼筆記本 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言